#!/usr/bin/env python
# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

# Script that automatically pulls and uploads all upstream direct and indirect
# branches into the current branch.
#
# Example:
#
#   $ git branch -vv
#   * feature_final    xxxxxxxxx [feature_prereq_c: ...] ...
#     feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
#     feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
#     feature_prereq_a xxxxxxxxx [main: ...] ...
#     main             xxxxxxxxx [origin/main] ...
#
# Executing `git_sync_cl_chain.py -m <message>` causes the following chain of
# commands to be executed:
#
#   $ git checkout feature_prereq_a; git pull; git cl upload -m <message>
#   $ git checkout feature_prereq_b; git pull; git cl upload -m <message>
#   $ git checkout feature_prereq_c; git pull; git cl upload -m <message>
#   $ git checkout feature_final;    git pull; git cl upload -m <message>

import optparse
import os
import sys

import defines
import utils

REPO_ROOT = defines.REPO_ROOT

class Repo(object):
  def __init__(self, name, is_current, upstream):
    self.name = name
    self.is_current = is_current
    self.upstream = upstream

def ParseOptions(argv):
  result = optparse.OptionParser()
  result.add_option('--delete', '-d',
                    help='Delete closed branches',
                    choices=['y', 'n', 'ask'],
                    default='ask')
  result.add_option('--leave_upstream', '--leave-upstream',
                    help='To not update the upstream of the first open branch',
                    action='store_true')
  result.add_option('--message', '-m',
                    help='Message for patchset', default='Sync')
  result.add_option('--rebase',
                    help='To use `git pull --rebase` instead of `git pull`',
                    action='store_true')
  result.add_option('--no_upload', '--no-upload',
                    help='Disable uploading to Gerrit', action='store_true')
  result.add_option('--skip_main', '--skip-main',
                    help='Disable syncing for main',
                    action='store_true')
  (options, args) = result.parse_args(argv)
  options.upload = not options.no_upload
  assert options.delete != 'y' or not options.leave_upstream, (
      'Inconsistent options: cannot leave the upstream of the first open ' +
      'branch (--leave_upstream) and delete the closed branches at the same ' +
      'time (--delete).')
  assert options.message, 'A message for the patchset is required.'
  assert len(args) == 0
  return options

def main(argv):
  options = ParseOptions(argv)
  with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
    branches = [
        parse(line)
        for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)]

    current_branch = None
    for branch in branches:
      if branch.is_current:
        current_branch = branch
        break
    assert current_branch is not None

    if current_branch.upstream == None:
      print('Nothing to sync')
      return

    stack = []
    while current_branch:
      stack.append(current_branch)
      if current_branch.upstream is None:
        break
      current_branch = get_branch_with_name(current_branch.upstream, branches)

    closed_branches = []
    has_seen_local_branch = False # A branch that is not uploaded.
    has_seen_open_branch = False # A branch that is not closed.
    while len(stack) > 0:
      branch = stack.pop()

      utils.RunCmd(['git', 'checkout', branch.name], quiet=True)

      status = get_status_for_current_branch()
      print('Syncing %s (status: %s)' % (branch.name, status))

      pull_for_current_branch(branch, options)

      if branch.name == 'main':
        continue

      if status == 'closed':
        assert not has_seen_local_branch, (
            'Unexpected closed branch %s after new branch' % branch.name)
        assert not has_seen_open_branch, (
            'Unexpected closed branch %s after open branch' % branch.name)
        closed_branches.append(branch.name)
        continue

      if not options.leave_upstream:
        if not has_seen_open_branch and len(closed_branches) > 0:
          print(
              'Setting upstream for first open branch %s to main'
                  % branch.name)
          set_upstream_for_current_branch_to_main()

      has_seen_open_branch = True
      has_seen_local_branch = has_seen_local_branch or (status == 'None')

      if options.upload and status != 'closed':
        if has_seen_local_branch:
          print(
              'Cannot upload branch %s since it comes after a local branch'
                  % branch.name)
        else:
          utils.RunCmd(
              ['git', 'cl', 'upload', '-m', options.message], quiet=True)

    if get_delete_branches_option(closed_branches, options):
      delete_branches(closed_branches)

    utils.RunCmd(['git', 'cl', 'issue'])

def delete_branches(branches):
  assert len(branches) > 0
  cmd = ['git', 'branch', '-D']
  cmd.extend(branches)
  utils.RunCmd(cmd, quiet=True)

def get_branch_with_name(name, branches):
  for branch in branches:
    if branch.name == name:
      return branch
  return None

def get_delete_branches_option(closed_branches, options):
  if len(closed_branches) == 0:
    return False
  if options.leave_upstream:
    return False
  if options.delete == 'y':
    return True
  if options.delete == 'n':
    return False
  assert options.delete == 'ask'
  print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches))
  answer = sys.stdin.read(1)
  return answer.lower() == 'y'

def get_status_for_current_branch():
  return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], quiet=True)[0].strip()

def pull_for_current_branch(branch, options):
  if branch.name == 'main' and options.skip_main:
    return
  rebase_args = ['--rebase'] if options.rebase else []
  utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)


def set_upstream_for_current_branch_to_main():
  utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True)

# Parses a line from the output of `git branch -vv`.
#
# Example output ('*' denotes the current branch):
#
#   $ git branch -vv
#   * feature_final    xxxxxxxxx [feature_prereq_c: ...] ...
#     feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
#     feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
#     feature_prereq_a xxxxxxxxx [main: ...] ...
#     main             xxxxxxxxx [origin/main] ...
def parse(line):
  is_current = False
  if line.startswith('*'):
    is_current = True
    line = line[1:].lstrip()
  else:
    line = line.lstrip()

  name_end_index = line.index(' ')
  name = line[:name_end_index]
  line = line[name_end_index:].lstrip()

  if '[' in line:
    line = line[line.index('[')+1:]

    if ':' in line:
      upstream = line[:line.index(':')]
      return Repo(name, is_current, upstream)

    if ']' in line:
      upstream = line[:line.index(']')]
      return Repo(name, is_current, upstream)

  return Repo(name, is_current, None)

if __name__ == '__main__':
  sys.exit(main(sys.argv[1:]))
